1 module commons; 2 public import arsd.terminal : Color, ConsoleOutputType, ConsoleInputFlags; 3 public static import arsd.terminal; 4 public import std.array:join, split; 5 public import std.json; 6 public import std.path; 7 public import std.process; 8 public static import std.file; 9 public import default_handlers; 10 public import redub.api; 11 12 13 enum hipremeEngineRepo = "https://github.com/MrcSnm/HipremeEngine.git"; 14 enum ConfigFile = "gamebuild.json"; 15 16 JSONValue engineConfig; 17 Config configs; 18 19 string pathBeforeNewLdc; 20 21 struct Terminal 22 { 23 import std.stdio; 24 import core.sync.mutex; 25 26 arsd.terminal.Terminal* arsdTerminal; 27 Mutex mtx; 28 this(arsd.terminal.Terminal* arsdTerminal) 29 { 30 this.arsdTerminal = arsdTerminal; 31 mtx = new Mutex(); 32 } 33 34 void color(Color main, Color secondary) 35 { 36 if(arsdTerminal) synchronized(mtx) 37 arsdTerminal.color(main, secondary); 38 } 39 int cursorY() 40 { 41 if(arsdTerminal) synchronized(mtx) 42 { 43 arsdTerminal.updateCursorPosition(); 44 return arsdTerminal.cursorY; 45 } 46 return 0; 47 } 48 string getline(string message) 49 { 50 if(arsdTerminal) synchronized(mtx) return arsdTerminal.getline(message); 51 std.stdio.writeln("Can't get line with message [", message, "]"); 52 return ""; 53 } 54 void moveTo(int x, int y){if(arsdTerminal) synchronized(mtx) arsdTerminal.moveTo(x, y);} 55 void clear(){if(arsdTerminal) synchronized(mtx) arsdTerminal.clear();} 56 void write(T...)(T args) 57 { 58 if(arsdTerminal) synchronized(mtx) arsdTerminal.write(args); 59 else std.stdio.write(args); 60 } 61 void flush() 62 { 63 if(arsdTerminal) synchronized(mtx) 64 { 65 arsdTerminal.flush(); 66 arsdTerminal.updateCursorPosition(); 67 } 68 } 69 70 int wait(Pid pid) 71 { 72 this.flush; 73 return std.process.wait(pid); 74 } 75 76 void hideCursor(){ if(arsdTerminal) synchronized(mtx) arsdTerminal.hideCursor();} 77 void showCursor(){ if(arsdTerminal) synchronized(mtx) arsdTerminal.showCursor();} 78 void clearToEndOfLine() 79 { 80 if(arsdTerminal) synchronized(mtx) arsdTerminal.clearToEndOfLine(); 81 flush(); 82 } 83 void clearLine() 84 { 85 moveTo(0, cursorY); 86 clearToEndOfLine(); 87 flush(); 88 } 89 90 void writeln(T...)(T args) 91 { 92 if (arsdTerminal) synchronized(mtx) arsdTerminal.writeln(args); 93 else std.stdio.writeln(args); 94 } 95 ~this() 96 { 97 showCursor(); 98 if(arsdTerminal) synchronized(mtx) destroy(*arsdTerminal); 99 mtx = null; 100 } 101 } 102 103 struct RealTimeConsoleInput 104 { 105 private arsd.terminal.RealTimeConsoleInput* input; 106 this(arsd.terminal.RealTimeConsoleInput* input){this.input = input;} 107 dchar getch() 108 { 109 if(input) return input.getch(); 110 return '\0'; 111 } 112 ~this() 113 { 114 if(input) destroy(*input); 115 } 116 } 117 118 struct TerminalColors 119 { 120 private Terminal* _t; 121 this(Color main, Color secondary, ref Terminal terminal) 122 { 123 _t = &terminal; 124 _t.color(main, secondary); 125 } 126 ~this() 127 { 128 _t.color(Color.DEFAULT, Color.DEFAULT); 129 _t.flush(); 130 } 131 } 132 133 struct WorkingDir 134 { 135 private string _currDir; 136 this(string targetDir) 137 { 138 _currDir = std.file.getcwd(); 139 std.file.chdir(targetDir); 140 } 141 ~this(){std.file.chdir(_currDir);} 142 } 143 144 enum ChoiceResult 145 { 146 None, 147 Continue, 148 Error, 149 Back, 150 } 151 152 struct Choice 153 { 154 string name; 155 ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected; 156 bool shouldTime; 157 string function() updateChoice; 158 bool scriptOnly; 159 bool disableSelectedConfigCache; 160 161 162 163 164 165 166 this(string name, 167 ChoiceResult function(Choice* self, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions opts) onSelected, 168 bool shouldTime = false, 169 string function() updateChoice = null, bool scriptOnly = false, bool disableSelectedConfigCache = false) 170 { 171 this.name = updateChoice ? updateChoice() : name; 172 this.onSelected = onSelected; 173 this.shouldTime = shouldTime; 174 this.updateChoice = updateChoice; 175 this.scriptOnly = scriptOnly; 176 this.disableSelectedConfigCache = disableSelectedConfigCache; 177 } 178 179 bool opEquals(ref const Choice other) const 180 { 181 return name == other.name; 182 } 183 bool opEquals(string choiceName) const 184 { 185 return name == choiceName; 186 } 187 } 188 189 struct Config 190 { 191 JSONValue cfg; 192 193 this(JSONValue js) 194 { 195 cfg = js; 196 if(!("windows" in cfg)) cfg.object["windows"] = JSONValue(string[string].init); 197 if(!("posix" in cfg)) cfg["posix"] = JSONValue(string[string].init); 198 } 199 string toString() 200 { 201 return cfg.toPrettyString(JSONOptions.doNotEscapeSlashes); 202 } 203 204 auto opBinaryRight(string op, R)(const R rhs) const 205 if(op == "in") 206 { 207 version(Windows){return rhs in cfg["windows"];} 208 else version(Posix){return rhs in cfg["posix"];} 209 else static assert(false, "OS not supported"); 210 } 211 212 ref auto opIndexAssign(T)(T value, string obj) 213 { 214 version(Windows){return cfg["windows"][obj] = value;} 215 else version(Posix){return cfg["posix"][obj] = value;} 216 else static assert(false, "OS not supported"); 217 } 218 219 ref auto opIndex(string obj) 220 { 221 version(Windows){return cfg["windows"][obj];} 222 else version(Posix){return cfg["posix"][obj];} 223 else static assert(false, "OS not supported"); 224 } 225 } 226 227 struct CompilationOptions 228 { 229 bool dubVerbose; 230 bool force; 231 bool tempBuild; 232 string getDubOptions() const 233 { 234 string ret; 235 if(force) ret~= " --force"; 236 if(tempBuild) ret~= " --temp-build"; 237 if(dubVerbose) ret~= " --verbose"; 238 return ret; 239 } 240 } 241 242 T[] unique(T)(T[] input) 243 { 244 bool[T] seen; 245 T[] ret; 246 foreach(v; input) 247 { 248 if(!(v in seen)) 249 { 250 seen[v] = true; 251 ret~= v; 252 } 253 } 254 return ret; 255 } 256 257 size_t selectChoiceBase(ref Terminal terminal, ref RealTimeConsoleInput input, Choice[] choices, 258 string selectionTitle, size_t selectedChoice = 0) 259 { 260 bool exit; 261 enum ESC = 983067; 262 enum ArrowUp = 983078; 263 enum ArrowDown = 983080; 264 enum SelectionHint = "Select an option by using W/S or Arrow Up/Down and choose it by pressing Enter."; 265 266 static bool isFirst = true; 267 static void changeChoiceClear(ref Terminal t, Choice[] choices, string title, Choice current, Choice next, int nextCursorOffset, bool bClear) 268 { 269 t.color(Color.DEFAULT, Color.DEFAULT); 270 if(bClear) 271 t.clear(); 272 t.writelnHighlighted(title); 273 t.writeln(SelectionHint); 274 foreach(i, c; choices) 275 { 276 if(c.name == next.name) with(TerminalColors(Color.green, Color.DEFAULT, t)) 277 t.writeln(">> ", c.name); 278 else t.writeln(c.name); 279 } 280 t.flush; 281 } 282 283 if(!isFirst) 284 terminal.clear(); 285 int startLine = terminal.cursorY; 286 terminal.flush(); 287 terminal.moveTo(0, startLine + cast(int)selectedChoice); 288 terminal.hideCursor(); 289 290 291 size_t oldChoice = selectedChoice; 292 while(!exit) 293 { 294 changeChoiceClear(terminal, choices, selectionTitle, choices[oldChoice], choices[selectedChoice], cast(int)(cast(long)selectedChoice-oldChoice), !isFirst); 295 isFirst = false; 296 oldChoice = selectedChoice; 297 298 CheckInput: 299 size_t choice = input.getch; 300 switch(choice) 301 { 302 case 'w', 'W', ArrowUp: 303 selectedChoice = (selectedChoice + choices.length - 1) % choices.length; 304 break; 305 case 's', 'S', ArrowDown: 306 selectedChoice = (selectedChoice+1) % choices.length; 307 break; 308 case ESC: 309 selectedChoice = choices.length - 1; 310 exit = true; 311 break; 312 case '\n': 313 exit = true; 314 break; 315 default: 316 goto CheckInput; 317 } 318 } 319 import std.algorithm.searching; 320 terminal.moveTo(0, cast(int)startLine); 321 //Title + SelectionHint 322 foreach(i; 0..choices.length+ ( count(selectionTitle, "\n")+2)) 323 terminal.moveTo(0, cast(int)(startLine+i)), terminal.clearToEndOfLine(); 324 terminal.moveTo(0, cast(int)startLine+1); //Jump title 325 terminal.writelnSuccess(">> ", choices[selectedChoice].name); 326 327 terminal.showCursor(); 328 return selectedChoice; 329 } 330 331 string[] getProjectsAvailable() 332 { 333 import std.array; 334 import std.algorithm; 335 if(!("projectsAvailable" in configs)) 336 return []; 337 338 string[] existing = array(configs["projectsAvailable"].array.map!(v => v.str).filter!(v => std.file.exists(v))); 339 if(existing.length != configs["projectsAvailable"].array.length) 340 { 341 configs["projectsAvailable"] = JSONValue(existing); 342 updateConfigFile(); 343 } 344 return existing; 345 } 346 347 string getValidPath(ref Terminal t, string pathRequired) 348 { 349 string path; 350 while(true) 351 { 352 path = t.getline(pathRequired); 353 if(std.file.exists(path)) 354 return path; 355 } 356 } 357 358 bool filesExists(string basePath, scope immutable string[] files...) 359 { 360 foreach(f; files) 361 { 362 auto temp = buildNormalizedPath(basePath, f); 363 if(!std.file.exists(temp)) return false; 364 } 365 return true; 366 } 367 368 string getFirstExisting(string basePath, scope string[] tests...) 369 { 370 foreach(t; tests) 371 { 372 auto temp = buildNormalizedPath(basePath, t); 373 if(std.file.exists(temp)) return temp; 374 } 375 return ""; 376 } 377 378 string getHipPath(scope string[] paths...) 379 { 380 return buildPath([configs["hipremeEnginePath"].str] ~ paths); 381 } 382 383 string getFirstExistingVar(scope string[] vars...) 384 { 385 foreach(variable; vars) 386 { 387 if(variable in environment) 388 return environment[variable]; 389 } 390 return ""; 391 } 392 393 394 395 bool hasLdc() 396 { 397 return ("ldcPath" in configs) !is null; 398 } 399 400 bool dbgExecuteShell(scope const(char)[] command, ref Terminal t, const string[string] env = null) 401 { 402 t.writeln("Executing command: ", command); 403 auto ret = executeShell(command, env); 404 if(ret.status) 405 { 406 t.writelnError(cast(string)("Command '"~command~"' failed with: "~ ret.output)); 407 t.flush; 408 } 409 return ret.status == 0; 410 } 411 412 string findProgramPath(string program) 413 { 414 import std.algorithm:countUntil; 415 import std.process; 416 string searcher; 417 version(Windows) searcher = "where"; 418 else version(Posix) searcher = "which"; 419 else static assert(false, "No searcher program found in this OS."); 420 auto shellRes = executeShell(searcher ~" " ~ program, 421 [ 422 "PATH": environment["PATH"] 423 ]); 424 if(shellRes.status == 0) 425 return shellRes.output[0..shellRes.output.countUntil("\n")]; 426 return null; 427 } 428 429 void writelnHighlighted(ref Terminal t, scope string[] what...) 430 { 431 with(TerminalColors(Color.yellow, Color.DEFAULT, t)) 432 t.writeln(what.join()); 433 } 434 435 void writelnSuccess(ref Terminal t, scope string[] what...) 436 { 437 with(TerminalColors(Color.green, Color.DEFAULT, t)) 438 t.writeln(what.join()); 439 } 440 441 void writelnError(ref Terminal t, scope string[] what...) 442 { 443 with(TerminalColors(Color.red, Color.DEFAULT, t)) 444 t.writeln(what.join()); 445 } 446 447 auto timed(T)(ref Terminal t, scope lazy T val){return timed(t, "", val);} 448 auto timed(T)(ref Terminal t, string measuringWhat, scope lazy T val) 449 { 450 import std.datetime.stopwatch; 451 StopWatch sw = StopWatch(AutoStart.yes); 452 static if(is(T == void)) 453 { 454 val; 455 t.writeln(measuringWhat, sw.peek.total!"msecs", "ms"); 456 } 457 else 458 { 459 auto ret = val; 460 t.writeln(measuringWhat, sw.peek.total!"msecs", "ms"); 461 return ret; 462 } 463 } 464 465 466 467 struct Session 468 { 469 struct Cache 470 { 471 size_t line; 472 string file; 473 } 474 bool[Cache] cache; 475 } 476 private __gshared Session session; 477 478 void cached(scope void delegate() dg, string f = __FILE__, size_t l = __LINE__) 479 { 480 if(!(Session.Cache(l, f) in session.cache)) 481 { 482 session.cache[Session.Cache(l, f)] = true; 483 dg(); 484 } 485 } 486 487 /** 488 * Clears all cache. 489 * This may be useful after a dub.template.json was already generated. 490 * Or for example, after changing the current game. 491 */ 492 void clearCache() 493 { 494 session.cache.clear; 495 } 496 497 bool pollForExecutionPermission(ref Terminal t, ref RealTimeConsoleInput input, string operation) 498 { 499 t.writelnHighlighted(operation~" [Y]es/[N]o"); 500 t.flush; 501 while(true) 502 { 503 switch(input.getch) 504 { 505 case 'y', 'Y': return true; 506 case 'n', 'N': return false; 507 default: break; 508 } 509 } 510 } 511 512 bool extractZipToFolder(string zipPath, string outputDirectory, ref Terminal t) 513 { 514 import std.zip; 515 ZipArchive zip = new ZipArchive(std.file.read(zipPath)); 516 if(!std.file.exists(outputDirectory)) 517 { 518 t.writeln("Creating directory ", outputDirectory); 519 t.flush; 520 std.file.mkdirRecurse(outputDirectory); 521 } 522 foreach(fileName, archiveMember; zip.directory) 523 { 524 string outputFile = buildNormalizedPath(outputDirectory, fileName); 525 if(!std.file.exists(outputFile)) 526 { 527 if(archiveMember.expandedSize == 0) 528 std.file.mkdirRecurse(outputFile); 529 else 530 { 531 string currentDirName = outputFile; 532 ///For some reason on linux it thinks that .a files are directories 533 t.writeln("Extracting ", fileName); 534 t.flush; 535 currentDirName = currentDirName.dirName; 536 if(!std.file.exists(currentDirName)) 537 std.file.mkdirRecurse(currentDirName); 538 std.file.write(outputFile, zip.expand(archiveMember)); 539 } 540 } 541 } 542 return true; 543 } 544 545 546 bool isCompactedFile(string fileName) 547 { 548 import std.path; 549 switch(fileName.extension) 550 { 551 case ".gz", ".xz", ".zip", ".7zip", ".7z": return true; 552 default: return false; 553 } 554 } 555 556 557 bool extractToFolder(string zPath, string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input) 558 { 559 import features._7zip; 560 import std.path; 561 switch(zPath.extension) 562 { 563 case ".gz", ".xz": 564 version(Posix) 565 { 566 return extractTarGzToFolder(zPath, outputDirectory, t); 567 } 568 else version(Windows) 569 { 570 ///Handles .gz, .xz 571 if(!extract7ZipToFolder.execute(t, input, zPath, dirName(zPath))) 572 return false; 573 t.writelnSuccess("Extracted to ", dirName(zPath)); 574 //Now handle tar 575 return extractTarToFolder(t, zPath[0..$-".xz".length], outputDirectory); 576 } 577 else assert(false, "No .tar.gz support on non Posix"); 578 case ".zip": 579 return extractZipToFolder(zPath, outputDirectory, t); 580 case ".7zip", ".7z": 581 return extract7ZipToFolder.execute(t, input, zPath, outputDirectory); 582 default: 583 t.writelnError("Could not detect compressed archive type for "~zPath); 584 return false; 585 } 586 } 587 588 string executableExtension(string path) 589 { 590 version(Windows) return path~".exe"; 591 return path; 592 } 593 594 version(Posix) 595 bool extractTarGzToFolder(string tarGzPath, string outputDirectory, ref Terminal t) 596 { 597 if(!std.file.exists(tarGzPath)) 598 { 599 t.writelnError("File ", tarGzPath, " does not exists."); 600 return false; 601 } 602 t.writeln("Extracting ", tarGzPath, " to ", outputDirectory); 603 t.flush; 604 std.file.mkdirRecurse(outputDirectory); 605 return dbgExecuteShell("tar -xf "~tarGzPath~" -C "~outputDirectory, t); 606 } 607 608 bool extractTarToFolder(ref Terminal t, string tarPath, string outputDirectory) 609 { 610 import archive.tar; 611 try 612 { 613 auto tar = new TarArchive(std.file.read(tarPath)); 614 t.writeln("Extracting ", tarPath, " to ", outputDirectory); 615 foreach(file; tar.files) 616 { 617 if(file.path == "@LongLink") 618 continue; 619 string outputPath = buildNormalizedPath(outputDirectory, file.path); 620 if(std.file.exists(outputPath)) 621 continue; 622 string targetDir = dirName(outputPath); 623 if(!std.file.exists(targetDir)) 624 t.writeln("\t->", targetDir); 625 std.file.mkdirRecurse(targetDir); 626 std.file.write(outputPath, file.data); 627 } 628 } 629 catch(Exception e) 630 { 631 t.writelnError("File ", tarPath, " does not exists, ", e.toString); 632 return false; 633 } 634 return true; 635 } 636 637 bool isRecognizedExtension(string ext) 638 { 639 switch(ext) 640 { 641 case ".7z", ".7zip", ".tar", ".xz", ".zf", ".bz", ".gz", ".zip": return true; 642 default: return false; 643 } 644 } 645 646 /** 647 * Removes the extension (while keeping numeric extensions such as dmd-2.105.0) 648 * Params: 649 * input = Input to remove extension 650 * Returns: 651 */ 652 string removeExtension(string input) 653 { 654 import std.string:isNumeric; 655 string ext; 656 while((ext = input.extension).length && ext.isRecognizedExtension) 657 input = input.setExtension(""); 658 return input; 659 } 660 661 /** 662 * 663 * Params: 664 * purpose = A message for the user to understand what is happening 665 * link = The link to file which will be downloaded to a temp dir 666 * outputName = A file name with a compressed archive extension (e.g: .zip, .7z, .tar.xz) 667 * outputDirectory = Where the file from outputName will be extracted 668 * t = Terminal 669 * input = RealTimeInput 670 * Returns: 671 */ 672 bool installFileTo(string purpose, string link, string outputName, 673 string outputDirectory, ref Terminal t, ref RealTimeConsoleInput input) 674 { 675 string downloadDir = buildNormalizedPath(std.file.tempDir, outputName); 676 if(!downloadFileIfNotExists(purpose, link, downloadDir, t, input)) 677 { 678 t.writelnError("Download failed"); 679 t.flush; 680 return false; 681 } 682 683 684 outputName = outputName.removeExtension; 685 686 string installDir = buildNormalizedPath(outputDirectory, outputName); 687 if(!extractToFolder(downloadDir, installDir, t, input)) 688 { 689 t.writelnError("Could not extract ",downloadDir, " to ", installDir); 690 return false; 691 } 692 693 return true; 694 } 695 696 bool makeFileExecutable(string filePath) 697 { 698 version(Windows) return true; 699 version(Posix) 700 { 701 if(!std.file.exists(filePath)) return false; 702 import std.conv:octal; 703 std.file.setAttributes(filePath, octal!700); 704 return true; 705 } 706 } 707 708 bool downloadFileIfNotExists( 709 string purpose, string link, string outputName, 710 ref Terminal t, ref RealTimeConsoleInput input 711 ) 712 { 713 import std.net.curl; 714 import std.conv:to; 715 string theDir = dirName(outputName); 716 if(!std.file.exists(theDir)) 717 std.file.mkdirRecurse(theDir); 718 if(!std.file.exists(outputName)) 719 { 720 if(!pollForExecutionPermission(t, input, "Your system will download a file: "~ purpose~"("~link~")")) 721 return false; 722 t.writelnHighlighted("Download started."); 723 t.flush; 724 size_t time = downloadWithProgressBar(t, link, outputName); 725 t.writelnSuccess("\nDownload succeeded after ", time.to!string, " msecs!"); 726 t.flush; 727 } 728 return true; 729 } 730 731 private void terminalProgressBar(ref Terminal t, float percentage, ubyte ticksCount = 32) 732 { 733 assert(percentage <= 1.0 && percentage >= 0, "Invalid percentage."); 734 735 ubyte drawnTicks = cast(ubyte)(ticksCount*percentage); 736 int line = t.cursorY; 737 t.moveTo(0, line); 738 t.clearToEndOfLine(); 739 t.write("<"); 740 foreach(int i; 0..ticksCount) 741 { 742 t.color(i < drawnTicks ? Color.green : Color.red, Color.DEFAULT); 743 t.write(i < drawnTicks ? "=" : "."); 744 } 745 t.color(Color.DEFAULT, Color.DEFAULT); 746 t.write("> (", percentage*100, "%)"); 747 t.flush(); 748 } 749 750 ///Adds a path to PATH 751 void addToPath(string pathToAdd) 752 { 753 import std.array:join; 754 string concatPath = ":"; 755 version(Windows) concatPath = ";"; 756 environment["PATH"] = join([ 757 pathToAdd, 758 environment["PATH"] 759 ], concatPath); 760 } 761 762 size_t downloadWithProgress(string url, string saveToPath, void delegate(float t) onProgress, size_t updateDelay = 125) 763 { 764 import std.net.curl:HTTP; 765 import core.time:dur; 766 import std.datetime.stopwatch:StopWatch, AutoStart; 767 import std.stdio : File; 768 size_t received, contentLength; 769 HTTP conn = HTTP(); 770 conn.url = url; 771 772 string targetDir = dirName(saveToPath); 773 if(!std.file.exists(targetDir)) 774 std.file.mkdirRecurse(targetDir); 775 776 static void writer(string path) 777 { 778 auto f = File(path, "wb"); 779 while(true) 780 { 781 immutable(ubyte)[] data = receiveOnly!(immutable(ubyte)[]); 782 if(data.length == 0) 783 break; 784 f.rawWrite(data); 785 } 786 ownerTid.send(true); 787 } 788 auto writerTid = spawn(&writer, saveToPath); 789 StopWatch updateDelayChecker = StopWatch(AutoStart.yes); 790 size_t downloadTime; 791 conn.onReceive = (ubyte[] data) 792 { 793 import std.conv:to; 794 import std.stdio; 795 if(contentLength == 0) 796 contentLength = conn.responseHeaders["content-length"].to!size_t; 797 received+= data.length; 798 if(updateDelayChecker.peek.total!"msecs" >= updateDelay || received == contentLength) 799 { 800 downloadTime+= updateDelayChecker.peek.total!"msecs"; 801 onProgress(cast(float)received/contentLength); 802 updateDelayChecker.reset(); 803 } 804 send(writerTid, data.idup); 805 return data.length; 806 }; 807 conn.perform(); 808 send(writerTid, (immutable(ubyte)[]).init); 809 receiveTimeout(dur!"msecs"(1000), (bool){}); //Block until finish 810 return downloadTime; 811 } 812 813 /** 814 * Same as std.net.curl.download 815 * Difference is that it shows a progress bar while downloading. 816 * Returns the time needed to download. 817 */ 818 size_t downloadWithProgressBar(ref Terminal t, string url, string saveToPath, size_t updateDelay = 125) 819 { 820 t.hideCursor(); 821 scope(exit) 822 { 823 t.showCursor(); 824 t.writeln(""); 825 } 826 return downloadWithProgress(url, saveToPath, (float progress) 827 { 828 terminalProgressBar(t, progress); 829 }); 830 } 831 832 833 private string getConfigPath() 834 { 835 import core.runtime; 836 static string cfgPath; 837 if(cfgPath == "") 838 cfgPath = buildNormalizedPath(Runtime.args[0].dirName, ConfigFile); 839 return cfgPath; 840 } 841 private string getEngineConfigPath() 842 { 843 return getHipPath("bin" ,"desktop", "engine_opts.json"); 844 } 845 void updateEngineFile() 846 { 847 std.file.write(getEngineConfigPath, engineConfig.toPrettyString()); 848 } 849 void updateConfigFile() 850 { 851 if(!("defaultProject" in engineConfig) && "gamePath" in configs) 852 { 853 engineConfig["defaultProject"] = configs["gamePath"].str; 854 updateEngineFile(); 855 } 856 std.file.write(getConfigPath, configs.toString()); 857 } 858 string getSourceCodeEditor(string projectPath) 859 { 860 if(!("sourceCodeEditor" in configs) || configs["sourceCodeEditor"].str.length == 0) 861 { 862 string out_Editor; 863 if(getDefaultSourceEditor(buildNormalizedPath(projectPath, "source", "gamescript", "entry.d"), out_Editor)) 864 configs["sourceCodeEditor"] = out_Editor; 865 else 866 configs["sourceCodeEditor"] = ""; 867 updateConfigFile(); 868 } 869 870 return configs["sourceCodeEditor"].str; 871 } 872 873 bool openSourceCodeEditor(string projectPath) 874 { 875 string sourceEditor = getSourceCodeEditor(projectPath); 876 if(!sourceEditor.length) 877 return false; 878 879 spawnShell(sourceEditor.escapeShellCommand~" "~projectPath.escapeShellCommand); 880 return true; 881 } 882 883 884 string getGitExec() 885 { 886 if("git" in configs) 887 { 888 version(Windows) return buildNormalizedPath(configs["git"].str, "git.exe"); 889 else return buildNormalizedPath(configs["git"].str, "git"); 890 } 891 return "git "; 892 } 893 894 895 896 private ChoiceResult _backFn(Choice* c, ref Terminal t, ref RealTimeConsoleInput input, in CompilationOptions cOpts) 897 { 898 return ChoiceResult.Back; 899 } 900 Choice getBackChoice() 901 { 902 return Choice("Back", &_backFn, false, null, false, true); 903 } 904 905 906 string getDubPath() 907 { 908 string dub = buildNormalizedPath(configs["dubPath"].str, "dub"); 909 version(Windows) dub = dub.setExtension("exe"); 910 return dub; 911 } 912 913 bool writeTemplate(ref Terminal t, string projectPath, string enginePath) 914 { 915 import std.conv:to; 916 import template_processor; 917 string out_DubFile; 918 auto res = processTemplate(projectPath, enginePath, out_DubFile); 919 if(res != TemplateProcessorResult.success) 920 { 921 t.writelnError(res.to!string, ":", out_DubFile); 922 return false; 923 } 924 try std.file.write(buildNormalizedPath(projectPath, "dub.json"), out_DubFile); 925 catch(Exception e){ 926 t.writelnError("Could not write dub.json"); 927 return false; 928 } 929 return true; 930 } 931 932 private int execDubBase(ref Terminal t, in DubArguments dArgs) 933 { 934 if(absolutePath(configs["hipremeEnginePath"].str) != absolutePath(std.file.getcwd())) 935 if(std.file.exists("dub.template.json")) 936 return writeTemplate(t, std.file.getcwd(), configs["hipremeEnginePath"].str) ? 0 : -1; 937 return 0; 938 } 939 940 941 mixin template BuilderPattern(Struct) 942 { 943 static foreach(mem; __traits(allMembers, Struct)) 944 { 945 import std.traits:isFunction; 946 static if(!isFunction!(__traits(getMember, Struct, mem)) && mem[0] == '_') 947 { 948 mixin(typeof(__traits(getMember, Struct, mem)), " ", mem[1..$], "() { return ", mem, ";}", 949 Struct, " ", mem[1..$], "(", typeof(__traits(getMember, Struct, mem)), " arg )", 950 "{this.",mem, " = arg; return this;}"); 951 } 952 } 953 } 954 955 enum Compilers 956 { 957 automatic = 0, 958 ldc2, 959 dmd 960 } 961 962 963 immutable string[] compilers = ["auto", "ldc2", "dmd"]; 964 string getSelectedCompiler() 965 { 966 const(JSONValue)* c = "selectedCompiler" in configs; 967 if(!c) return "auto"; 968 return compilers[c.get!uint]; 969 } 970 971 972 struct DubArguments 973 { 974 string _command; 975 string _configuration; 976 CompilationOptions _opts; 977 string _dir; 978 string _preCommands; 979 string _compiler = "auto"; 980 string _arch; 981 string _build; 982 string _recipe; 983 string _runArgs; 984 bool _confirmKey; 985 bool _deep; 986 bool _parallel = true; 987 988 mixin BuilderPattern!(DubArguments); 989 990 string getCompiler() 991 { 992 string c = _compiler; 993 if(_compiler == "auto") 994 { 995 c = getSelectedCompiler(); 996 } 997 if(c == "auto") 998 { 999 if(_arch) 1000 c = "ldc2"; 1001 else 1002 return ""; 1003 } 1004 if(c == "ldc2") 1005 c = buildNormalizedPath(configs["ldcPath"].str, "bin", "ldc2".executableExtension); 1006 else if(c == "dmd") 1007 c = buildNormalizedPath(configs["dmdPath"].str, "dmd".executableExtension); 1008 return c; 1009 } 1010 1011 string getDubRunCommand() 1012 { 1013 string dub = getDubPath(); 1014 string a = command; ///Arguments 1015 compiler = getCompiler(); 1016 if(parallel) a~= " --parallel"; 1017 if(recipe) a~= " --recipe="~recipe; 1018 if(build) a~= " --build="~build; 1019 if(arch) a~= " --arch="~arch; 1020 if(compiler != "")a~= " --compiler="~compiler; 1021 if(deep) a~= " --deep"; 1022 if(configuration) a~= " -c "~configuration; 1023 if(opts != CompilationOptions.init) a~= opts.getDubOptions(); 1024 if(runArgs) a~= " -- "~runArgs; 1025 1026 1027 version(Windows) 1028 { 1029 if(confirmKey) a~= " && pause"; 1030 } 1031 else version(Posix) 1032 { 1033 if(confirmKey) a~= " && read -p \"Press any key to continue... \" -n1 -s"; 1034 } 1035 return preCommands~dub~" "~a; 1036 } 1037 } 1038 1039 int waitRedub(ref Terminal t, DubArguments dArgs, out ProjectDetails proj, string copyLinkerFilesTo = null) 1040 { 1041 import redub.logging; 1042 import redub.buildapi; 1043 import redub.api; 1044 if(execDubBase(t, dArgs) == -1) return -1; 1045 1046 setLogLevel(dArgs._opts.dubVerbose ? LogLevel.verbose : LogLevel.info); 1047 proj = resolveDependencies( 1048 dArgs._opts.force, 1049 os, 1050 CompilationDetails(dArgs.getCompiler(), null, dArgs._arch), 1051 ProjectToParse(dArgs._configuration, std.file.getcwd(), null, dArgs._recipe), 1052 InitialDubVariables.init, 1053 BuildType.profile_gc 1054 ); 1055 try { 1056 if(buildProject(proj).error) return 1; 1057 } 1058 catch(BuildException err) { return 1; } 1059 if(copyLinkerFilesTo.length) 1060 { 1061 import tools.copylinkerfiles; 1062 string[] linkerFiles; 1063 t.flush; 1064 timed(t, "Copying Linker Files ", 1065 { 1066 proj.getLinkerFiles(linkerFiles); 1067 copyLinkerFiles(linkerFiles, copyLinkerFilesTo); 1068 }()); 1069 t.flush; 1070 } 1071 return 0; 1072 } 1073 1074 void inParallel(scope void delegate()[] args...) 1075 { 1076 import std.parallelism; 1077 1078 // version(AArch64) 1079 // { 1080 // foreach(action; args) 1081 // action(); 1082 // } 1083 // else 1084 // { 1085 foreach(action; parallel(args)) 1086 action(); 1087 // } 1088 } 1089 1090 int waitDub(ref Terminal t, DubArguments dArgs, string copyLinkerFilesTo = null) 1091 { 1092 ///Detects the presence of a template file before executing. 1093 ProjectDetails d; 1094 if(dArgs._command.length >= 3 && dArgs._command[0..3] != "run") return waitRedub(t, dArgs, d, copyLinkerFilesTo); 1095 if(execDubBase(t, dArgs) == -1) return -1; 1096 string toExec = dArgs.getDubRunCommand(); 1097 t.writeln(toExec); 1098 t.flush; 1099 return t.wait(spawnShell(toExec)); 1100 } 1101 1102 int waitDubTarget(ref Terminal t, string target, DubArguments dArgs, string copyLinkerFilesTo = null) 1103 { 1104 return waitDub(t, dArgs.recipe(buildPath(getBuildTarget(target), "dub.json")), copyLinkerFilesTo); 1105 } 1106 1107 int waitAndPrint(ref Terminal t, Pid pid) 1108 { 1109 return wait(pid); 1110 } 1111 1112 public import std.concurrency; 1113 bool waitOperations(immutable bool delegate()[] operations) 1114 { 1115 foreach(op; operations) 1116 { 1117 spawn((bool delegate() targetOperation) 1118 { 1119 ownerTid.send(targetOperation()); 1120 }, op); 1121 } 1122 1123 foreach(i; 0..operations.length) 1124 if(!receiveOnly!bool) 1125 return false; 1126 return true; 1127 } 1128 1129 1130 void putResourcesIn(ref Terminal t, string where) 1131 { 1132 import tools.copyresources; 1133 string gPath = configs["gamePath"].str; 1134 if(!isAbsolute(gPath)) 1135 gPath = absolutePath(gPath); 1136 string resources = buildNormalizedPath(gPath, "assets"); 1137 if(!std.file.exists(resources)) 1138 std.file.mkdirRecurse(resources); 1139 copyResources(t, resources, where, false); 1140 } 1141 1142 void executeGameRelease(ref Terminal t) 1143 { 1144 import tools.releasegame; 1145 releaseGame(t, configs["gamePath"].str, getHipPath("build", "release_game"), false); 1146 } 1147 1148 1149 1150 string selectInFolder(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input, 1151 scope string[] extFilters = [".DS_Store"]) 1152 { 1153 import std.string; 1154 Choice[] choices; 1155 LISTING_FILE: foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow)) 1156 { 1157 foreach(f; extFilters) 1158 if(e.name.endsWith(f)) continue LISTING_FILE; 1159 choices~= Choice(e.name, null); 1160 } 1161 size_t choice; 1162 choice = selectChoiceBase(t, input, choices, selectWhat); 1163 1164 return choices[choice].name; 1165 } 1166 1167 /** 1168 * Main difference from selectInFolder is that it returns the choice and also acacepts extra choices. 1169 * Params: 1170 * selectWhat = Description 1171 * directory = Directory to iterate 1172 * t = 1173 * input = 1174 * extraChoices = May be used to go back or cancel process 1175 * Returns: Selected choice 1176 */ 1177 Choice* selectInFolderExtra(string selectWhat, string directory, ref Terminal t, ref RealTimeConsoleInput input, 1178 return scope Choice[] choices, scope Choice[] extraChoices, scope string[] extFilters = [".DS_Store"]) 1179 { 1180 import std.string; 1181 LISTING_FILES: 1182 foreach(std.file.DirEntry e; std.file.dirEntries(directory, std.file.SpanMode.shallow)) 1183 { 1184 foreach(f; extFilters) if(e.name.endsWith(f)) continue LISTING_FILES; 1185 choices~= Choice(e.name, null); 1186 } 1187 choices = (choices ~ extraChoices).unique; 1188 size_t choice; 1189 choice = selectChoiceBase(t, input, choices, selectWhat); 1190 1191 return &choices[choice]; 1192 } 1193 1194 1195 1196 version(Windows) 1197 { 1198 import std.windows.registry; 1199 Key windowsGetKeyWithPath(string[] path...) 1200 { 1201 Key hklm = Registry.localMachine; 1202 if(hklm is null) throw new Error("No HKEY_LOCAL_MACHINE in this system."); 1203 Key currKey = hklm; 1204 foreach(p; path) 1205 { 1206 try{ 1207 currKey = currKey.getKey(p); 1208 if(currKey is null) return null; 1209 } 1210 catch(Exception e) 1211 { 1212 return null; 1213 } 1214 } 1215 return currKey; 1216 } 1217 } 1218 1219 string getBuildTarget(string target = __MODULE__) 1220 { 1221 import std.string:split; 1222 import std.exception:enforce; 1223 target = target.split(".")[$-1]; 1224 string path = getHipPath("tools", "internal", "targets"); 1225 enforce(std.file.exists(path = buildPath(path, target)), "Target "~target~" does not exists."); 1226 return path; 1227 } 1228 1229 void outputTemplate(ref Terminal t, string templatePath) 1230 { 1231 import template_processor; 1232 string out_templ; 1233 1234 switch(processTemplate(templatePath, configs["hipremeEnginePath"].str, out_templ, [ 1235 "TARGET_PROJECT": configs["gamePath"].str 1236 ])) 1237 { 1238 case TemplateProcessorResult.invalid: 1239 t.writelnError("Could not process template from path ",templatePath); 1240 throw new Error("Can't build with invalid template."); 1241 case TemplateProcessorResult.notFound: 1242 t.writelnHighlighted("Template at ", templatePath, " not found, your game may use dub.json instead."); 1243 break; 1244 default: case TemplateProcessorResult.success: 1245 t.writelnSuccess("Template at path ", templatePath, " successfully generated"); 1246 std.file.write(buildPath(templatePath, "dub.json"), out_templ); 1247 break; 1248 } 1249 } 1250 1251 void outputTemplateForTarget(ref Terminal t, string target = __MODULE__) 1252 { 1253 import std.array:split; 1254 ///If it is the default, the target will be "targets.wasm", so, split and get the last. 1255 string buildTarget = getBuildTarget(target.split(".")[$-1]); 1256 t.writeln("Regenerating buildscript for target ", buildTarget); 1257 outputTemplate(t, buildTarget); 1258 } 1259 1260 bool isIpAddress(string ip) 1261 { 1262 import std.ascii; 1263 import std.conv:to; 1264 import std.string; 1265 1266 string[] parts = split(ip, "."); 1267 if (parts.length != 4) return false; 1268 1269 foreach (part; parts) { 1270 if (part.length == 0 || part.length > 3) 1271 return false; 1272 foreach(v; part) 1273 if(!isDigit(v)) 1274 return false; 1275 int value = to!int(part); 1276 if (value < 0 || value > 255 || (part[0] == '0' && part.length > 1)) return false; 1277 } 1278 1279 return true; 1280 } 1281 1282 void requireConfiguration( 1283 string cfgRequired, 1284 string purpose, 1285 ref Terminal t, 1286 ref RealTimeConsoleInput input, 1287 bool function(ref string inOutData) validation = null, 1288 string validationFailMsg = "Validation Failure" 1289 ) 1290 { 1291 import std.string:strip; 1292 1293 while(true) 1294 { 1295 string res = cfgRequired in configs ? configs[cfgRequired].str : null; 1296 if(res.length != 0) 1297 { 1298 if(validation && validation(res)) 1299 break; 1300 t.writelnError(validationFailMsg); 1301 } 1302 res = t.getline("Config '"~cfgRequired~"' is required for "~ purpose~ ". \n\tWrite here: ").strip(); 1303 1304 if(validation && validation(res)) 1305 { 1306 configs[cfgRequired] = res; 1307 updateConfigFile(); 1308 break; 1309 } 1310 1311 } 1312 } 1313 1314 /** 1315 * 1316 * Params: 1317 * original = The original path where the link will redirect 1318 * link = The path where the link will be created 1319 */ 1320 void symlink(string original, string link) 1321 { 1322 version(Posix){ 1323 std.file.symlink(original, link); 1324 } 1325 version(Windows) 1326 { 1327 import core.sys.windows.w32api:_WIN32_WINNT; 1328 static if(_WIN32_WINNT >= 0x600) //WindowsVista or later 1329 { 1330 import core.sys.windows.winbase; 1331 import core.sys.windows.windef:DWORD, MAX_PATH, LPWSTR; 1332 import std.utf:toUTF16z; 1333 import std.file:FileException; 1334 1335 DWORD typeFlag = 0; //File 1336 if(std.file.isDir(original)) 1337 typeFlag = SYMBOLIC_LINK_FLAG_DIRECTORY; 1338 typeFlag|= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; 1339 1340 if(link.length > MAX_PATH) link = `\\?\`~link; 1341 if(original.length > MAX_PATH) original = `\\?\`~original; 1342 1343 if(!CreateSymbolicLinkW(link.toUTF16z, original.toUTF16z, typeFlag)) 1344 { 1345 LPWSTR strBuffer; 1346 DWORD length = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, null, GetLastError(),0, cast(LPWSTR)&strBuffer, 0, null); 1347 wchar[] str = new wchar[length]; 1348 str[] = strBuffer[0..str.length]; 1349 LocalFree(strBuffer); 1350 import std.conv; 1351 throw new FileException(original, str.to!string); 1352 } 1353 } 1354 } 1355 } 1356 1357 1358 /** 1359 * May be used in future. Kept for reference. 1360 */ 1361 private bool hasAdminRights() 1362 { 1363 version(Windows) 1364 { 1365 ///https://stackoverflow.com/questions/8046097/how-to-check-if-a-process-has-the-administrative-rights 1366 import core.sys.windows.windows; 1367 bool hasRights = false; 1368 HANDLE hToken = NULL; 1369 if( OpenProcessToken( GetCurrentProcess( ),TOKEN_QUERY,&hToken ) ) { 1370 TOKEN_ELEVATION Elevation; 1371 DWORD cbSize = TOKEN_ELEVATION.sizeof; 1372 if(GetTokenInformation(hToken, TOKEN_INFORMATION_CLASS.TokenElevation, &Elevation, Elevation.sizeof, &cbSize)) 1373 hasRights = Elevation.TokenIsElevated == 1; 1374 } 1375 if(hToken) CloseHandle(hToken); 1376 return hasRights; 1377 } 1378 else return false; 1379 } 1380 1381 1382 static this() 1383 { 1384 configs = std.file.exists(getConfigPath) ? Config(parseJSON(std.file.readText(getConfigPath))) : Config(parseJSON("{}")); 1385 try engineConfig = std.file.exists(getEngineConfigPath) ? parseJSON(std.file.readText(getEngineConfigPath)) : parseJSON("{}"); 1386 catch(Exception e) engineConfig = parseJSON("{}"); 1387 }